"DIY" java: AspectJ、JDKProxy、CGLIB and ASM

1. 前言

1.1 说明

【说明:部分原创 & 整合了多篇网络文章,链接附在文末。】

AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等。

AOP 实现的关键就在于 AOP 框架自动创建的 AOP 代理,AOP 代理则可分为静态代理和动态代理两大类,其中静态代理是指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于 JDK 动态代理、CGLIB 等在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。

2. AspectJ

2.1 AspectJ 背景

AspectJ 是 eclipse 基金会的一个项目,官网就在 eclipse 官网里。官网里提供了一个aspectJ.jar的下载链接,但其实这个链接只是一个安装包,把安装包里的东西解压后就是一个文档 + 脚本 + jar包的程序包。

2.2 AspectJ 工具文件组织

1
2
3
4
5
6
7
8
9
10
11
12
myths@pc:~/aspectj1.8$ tree bin/ lib/
bin/
├── aj
├── aj5
├── ajbrowser
├── ajc
└── ajdoc
lib/
├── aspectjrt.jar
├── aspectjtools.jar
├── aspectjweaver.jar
└── org.aspectj.matcher.jar

这当中重点的文件是四个jar包中的前三个,bin文件夹中的脚本其实都是调用这些jar包的命令。

  • aspectjrt.jar 包主要是提供运行时的一些注解,静态方法等等东西,通常我们要使用aspectJ的时候都要使用这个包。
  • aspectjtools.jar 包主要是提供赫赫有名的ajc编译器,可以在编译期将将java文件或者class文件或者aspect文件定义的切面织入到业务代码中。通常这个东西会被封装进各种IDE插件或者自动化插件中。
  • aspectjweaverjar 包主要是提供了一个java agent用于在类加载期间织入切面(Load time weaving,LTW)。并且提供了对切面语法的相关处理等基础方法,供ajc使用或者供第三方开发使用。这个包一般我们不需要显式引用,除非需要使用 LTW。

上面的说明其实也就指出了aspectJ的几种标准的使用方法(参考文档):

  1. 编译时织入,利用ajc编译器替代javac编译器,直接将源文件(java或者aspect文件)编译成class文件并将切面织入进代码。
  2. 编译后织入,利用ajc编译器向javac编译期编译后的class文件或jar文件织入切面代码。
  3. 加载时织入,不使用ajc编译器,利用aspectjweaver.jar工具,使用java agent代理在类加载期将切面织入进代码。

2.3 下载 AspectJ

maven 仓库下载:https://repo1.maven.org/maven2/org/aspectj/aspectjtools/1.8.14/aspectjtools-1.8.14.jarhttps://mvnrepository.com/artifact/org.aspectj/aspectjtools

2.4 AspectJ DEMO

2.4.1 文件目录结构

1
2
3
4
5
6
7
- com.yupaopao.aspectj
|- aspectjweaver-1.8.14.jar
|- aspectjtools-1.8.14.jar
|- aspectjrt-1.8.14.jar
|- src
\ - AspectJMain
|- LogAspect

2.4.2 源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AspectJMain {
public void run() {
System.out.println("AspectJMain is running...");
}

public static void main(String[] args) {
AspectJMain app = new AspectJMain();
app.run();
}
}

public aspect LogAspect {

pointcut logPointcut():execution(* AspectJMain.run());

after():logPointcut(){
System.out.println("记录日志 ...");
}
}

2.4.3 执行

  1. 编译:(也可以直接使用 ajc -d . ./src )
1
java -jar aspectjtools-1.8.14.jar -cp aspectjrt-1.8.14.jar -sourceroots ./src -d .
  1. 运行:java -cp aspectjrt-1.8.14.jar:. AspectJMain
1
2
AspectJMain is running...
记录日志 ...
  1. decopmile:

https://github.com/java-decompiler/jd-gui/releases/download/v1.6.6/jd-gui-1.6.6.jar

1
java -jar jd-gui-1.6.6.jar

可以看到 class 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class AspectJMain {
public void run() {
try {
System.out.println("AspectJMain is running...");
} catch (Throwable throwable) {
LogAspect.aspectOf().ajc$after$LogAspect$1$9fd5dd97();
throw throwable;
}
LogAspect.aspectOf().ajc$after$LogAspect$1$9fd5dd97();
}

public static void main(String[] args) {
AspectJMain app = new AspectJMain();
app.run();
}
}

public class LogAspect {
private static Throwable ajc$initFailureCause;

public static final LogAspect ajc$perSingletonInstance;

static {
try {
ajc$postClinit();
} catch (Throwable throwable) {
ajc$initFailureCause = throwable = null;
}
}

public static LogAspect aspectOf() {
if (ajc$perSingletonInstance == null)
throw new NoAspectBoundException("LogAspect", ajc$initFailureCause);
return ajc$perSingletonInstance;
}

public static boolean hasAspect() {
return (ajc$perSingletonInstance != null);
}

private static void ajc$postClinit() {
ajc$perSingletonInstance = new LogAspect();
}

public void ajc$after$LogAspect$1$9fd5dd97() {
System.out.println("...");
}
}

虽然事实上这种基于aj文件的切面描述方法比基于java注解的切面描述方法用起来要灵活的多,但是由于他无法摆脱ajc的支持,而且本身不兼容java语法导致难以统一编码规范,加上需要较多额外的学习成本,因此事实上很多项目还是不怎么用这种方式,更多的还是采用了兼容java语法的用注解定义切面的方式。

2.5. 基于 java 注解的 AspectJ

下面我们主要还是着力考虑下基于java注解的切面使用方法。

2.5.1 准备

先建一个普通的项目看看,老样子,从maven的maven-archetype-quickstart开始,pom.xml,pom文件里我们一般只需要加上aspetjrt的依赖即可。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.mythsman.test</groupId>
<artifactId>aspect-test</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>work</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugins>
</build>
</project>

创建App.java文件:

1
2
3
4
5
6
7
8
9
10
11
public class App {

public void say() {
System.out.println("App say");
}

public static void main(String[] args) {
App app = new App();
app.say();
}
}

创建切面类AnnoAspect.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
public class AnnoAspect {

@Pointcut("execution(* com.mythsman.test.App.say(..))")
public void jointPoint() {
}

@Before("jointPoint()")
public void before() {
System.out.println("AnnoAspect before say");
}

@After("jointPoint()")
public void after() {
System.out.println("AnnoAspect after say");
}
}
1
2
3
4
5
6
7
8
9
10
11
当前项目结构应该是这样的:
.
├── pom.xml
├── src
│   └── main
│   ├── java
│   │   └── com
│   │   └── mythsman
│   │   └── test
│   │   └── App.java
│   │   ├── AnnoAspect.java

其实就是创建了一个对App类进行切面的AnnoAspect类,这个类需要加上@Aspect注解用以声明这是一个切面,以及其他相关切面语法。接下来我们就来尝试下三种不同的编译方式。

2.5.2 编译时织入

编译时织入其实就是使用ajc来进行编译,暂时不使用自动化构建工具,我们先在项目根目录下手动写一个编译脚本compile.sh:

1
2
3
4
5
6
#!/usr/bin/env bash
ASPECTJ_WEAVER=/home/myths/.m2/repository/org/aspectj/aspectjweaver/1.8.13/aspectjweaver-1.8.13.jar
ASPECTJ_RT=/home/myths/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
ASPECTJ_TOOLS=/home/myths/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar

java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -source 1.5 -sourceroots src/main/java/ -d target/classes

调用aspectjtools.jar,在-cp里指明aspectjrt.jar的路径,-source 1.5指明支持java1.5以后的注解,-sourceroots指明编译的文件夹,-d指明输出路径。

这样就会生成AnnoAspect.class和App.class两个文件。AnnoAspect.class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Aspect
public class AnnoAspect
{
public static AnnoAspect aspectOf()
{
if (ajc$perSingletonInstance == null) {
throw new NoAspectBoundException("com.mythsman.test.AnnoAspect", ajc$initFailureCause);
}
return ajc$perSingletonInstance;
}

public static boolean hasAspect()
{
return ajc$perSingletonInstance != null;
}

static
{
try
{
ajc$postClinit();
}
catch (Throwable localThrowable)
{
ajc$initFailureCause = localThrowable;
}
}

@Before("jointPoint()")
public void before()
{
System.out.println("AnnoAspect before say");
}

@After("jointPoint()")
public void after()
{
System.out.println("AnnoAspect after say");
}
}

App.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class App
{
public void say()
{
try
{
AnnoAspect.aspectOf().before();System.out.println("App say");
}
catch (Throwable localThrowable)
{
AnnoAspect.aspectOf().after();throw localThrowable;
}
AnnoAspect.aspectOf().after();
}

public static void main(String[] args)
{
App app = new App();
app.say();
}
}

我们发现ajc对AnnoAspect的处理方法与跟AjAspect的处理方法类似,都是将类声明成单例,并且识别AspectJ语法,将相关函数织入到App中。运行(在项目根目录执行):

1
2
3
4
5
$ java -cp ~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar:src/main/java/ com.mythsman.test.App 

AnnoAspect before say
App say
AnnoAspect after say

2.5.3 编译后织入

编译后织入其实就是在javac编译完成后,用ajc再去处理class文件得到新的、织入过切面的class文件。仍然是上面的项目,我们先用javac编译一下:

1
$ javac -cp ~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar  -d target/classes src/main/java/com/mythsman/test/*.java

编译成功后生成了AnnoAspect.class以及App.class。显然,这两个class文件反编译后还是源文件的样子,并没有什么用,因此这时候执行App的main函数发现切面并没有生效。因此我们仍然需要用ajc来处理:

1
2
3
4
5
6
!/usr/bin/env bash
ASPECTJ_WEAVER=/home/myths/.m2/repository/org/aspectj/aspectjweaver/1.8.13/aspectjweaver-1.8.13.jar
ASPECTJ_RT=/home/myths/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
ASPECTJ_TOOLS=/home/myths/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar

java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -source 1.5 -inpath target/classes -d target/classes

这样就把target/classes中原来的class文件替换成了织入后的class文件。反编译之后发现与采用编译期织入方法的结果基本相同。

2.5.4 加载时织入(LTW)

前两种织入方法都依赖于ajc的编译工具,LTW却通过java agent机制在内存中操作类文件,可以不需要ajc的支持做到动态织入。不过,这里有一个挺有意思的问题,我们知道编译期一定会编译AnnoAspect类,那么这时候通过切面语法我们就可以找到他要处理的App类,这大概就是编译阶段织入的大概流程。但是如果在类加载期处理的话,当类加载到App类的时候,我们并不知道这个类需要被AnnoAspect处理。

因此为了实现LTW,我们肯定要有个配置文件,来告诉类加载器,某某某切面需要优先考虑,他们很可能会影响其他的类。为了实现LTW,我们需要在资源目录下配置META-INF/aop.xml文件,来告知类加载器我们当前注册的切面。在上面的项目中,我们其实只需要创建src/main/resources/META-INF/aop.xml:

1
2
3
4
5
<aspectj>
<aspects>
<aspect name="com.mythsman.test.AnnoAspect"/>
</aspects>
</aspectj>

这样,我们就可以先使用javac编译源文件,再使用java agent在运行时织入:

1
2
3
4
5
6
#!/usr/bin/env bash
ASPECTJ_WEAVER=/home/myths/.m2/repository/org/aspectj/aspectjweaver/1.8.13/aspectjweaver-1.8.13.jar
ASPECTJ_RT=/home/myths/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
ASPECTJ_TOOLS=/home/myths/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar

java -javaagent:$ASPECTJ_WEAVER -cp $ASPECTJ_RT:target/classes/ com.mythsman.test.App

运行结果:

1
2
3
AnnoAspect before say
App say
AnnoAspect after say

当然,如果可以使用ajc的话,我们也可以通过-outxml参数来自动生成xml文件。

3. JDK 动态代理

JDK自从1.3版本开始,就引入了动态代理,并且经常被用来动态地创建代理。JDK的动态代理用起来非常简单,唯一限制便是使用动态代理的对象必须实现一个或多个接口。而CGLIB缺不必有此限制。

代理类具有以下属性:

  • 如果所有代理接口都是公共的,代理类是公共的、最终的,而不是抽象的。
  • 如果任何代理接口是非公共的,则代理类是非公共的、最终的,并且不是抽象的。
  • 以”$Proxy” 字符串开头的类名空间应该保留给代理类。
  • 代理类以相同的顺序实现在其创建时指定的接口。
  • 如果代理类实现了非公共接口,那么它将与该接口定义在同一个包中。否则,代理类的包也是未指定的。
  • 由于代理类实现了在其创建时指定的所有接口,调用getInterfaces其 Class对象将返回一个包含相同接口列表的数组(按照其创建时指定的顺序),调用 getMethods其Class对象将返回一个Method对象数组,其中包括这些接口中的所有方法,调用getMethod将按预期在代理接口中找到方法。
  • 每个代理类都有一个公共构造函数,它接受一个参数,即 interface 的一个实现InvocationHandler,以设置代理实例的调用处理程序。

当代理类的两个或多个接口包含具有相同名称和参数签名的方法时,代理类的接口的顺序就变得很重要。当在代理实例上调用重复方法时,Method包含代理类接口列表中的方法(直接或通过超接口继承)的最前面接口中方法的对象被传递给调用处理程序的invoke方法,而不管方法调用通过何种引用类型发生。

如果代理接口包含具有相同的名称和参数签名的方法hashCode,equals或toString,当这种方法在代理实例调用的 Method对象传递到调用处理程序将 java.lang.Object其声明类。换句话说,public && ! final方法在java.lang.Object 逻辑上位于所有代理接口之前,用于确定将哪个Method对象传递给调用处理程序。

注意 clone 方法默认是 protect 权限的,这意味着 proxy 对象的 clone 方法不能被访问。如果 proxy 实现了定义 Object clone(); 方法的接口,这个 clone() 需要开发人员自行实现,而不会调用到 Object#clone() 。

proxy 仍然是可以使用 synchronized 加锁的对象, wait/notify 方法是可用的。

另请注意,当重复方法被分派到调用处理程序时,如果 invoke方法抛出一个不可分配的异常类型(没有对应异常的接口方法),则 unchecked 异常会被包上 UndeclaredThrowableException。

3.1 基本使用

代理一般实现的模式为JDK静态代理:创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。之后再创建一个代理类,同时使其也实现这个接口。在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class JavaProxy {

public interface Echo {
String echo(String msg);
}

public static class EchoImpl implements Echo {

@Override
public String echo(String msg) {
return "echo:" + msg;
}

}

private static InvocationHandler echoHandler = new InvocationHandler() {
private Echo obj = new EchoImpl();

@Override
public Object invoke(Object proxy, Method method, Object[] args) {
try {
System.out.println("invoker triggered");
return method.invoke(obj, args);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException("echoHandler invoke exp", e);
}
}
};

public static void main(String[] args) {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?>[] interfaces = { Echo.class };
Echo proxy1 = (Echo) Proxy.newProxyInstance(classLoader, interfaces, echoHandler);
Echo proxy2 = (Echo) Proxy.newProxyInstance(classLoader, interfaces, echoHandler);
Echo proxy3 = (Echo) Proxy.newProxyInstance(classLoader, interfaces, (p, m, a) -> m.invoke(new Object(), a));
Object proxy4 = Proxy.newProxyInstance(classLoader, new Class<?>[] {}, (p, m, a) -> m.invoke(new Object(), a));
System.out.println(proxy1.echo("msg"));
System.out.println(proxy1.getClass() + "@" + proxy1.hashCode());
System.out.println(proxy2.getClass() + "@" + proxy2.hashCode());
System.out.println(proxy3.getClass() + "@" + proxy3.hashCode());
System.out.println(proxy4.getClass() + "@" + proxy4.hashCode());
System.out.println(proxy1 == proxy1);
System.out.println(proxy1 == proxy2);
System.out.println(proxy1 == proxy3);
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
invoker triggered
echo:msg
invoker triggered
class com.sun.proxy.$Proxy0@1282788025
invoker triggered
class com.sun.proxy.$Proxy0@1282788025
class com.sun.proxy.$Proxy0@519569038
class com.sun.proxy.$Proxy1@1870252780
true
false
false

注意 proxy1.hashCode() 获取到的哈希值实际上是 echoHandler 中 obj 的 hashCode,所以 proxy1 与 proxy2 在使用同一个 echoHandler 时 hashCode 相同,但是 == 地址判断返回 false。

其中接口数组的长度可以是 0,但是 invokeHandler 不可以为 null。

JDK动态代理是基于接口实现的。因为通过接口指向实现类实例的多态方式,可以有效地将具体实现与调用解耦,便于后期的修改和维护。

3.3 jdk 动态代理原理

3.3.1 InvocationHandler

在动态代理中,核心功能实现就是InvocationHandler。每一个代理的实例都会有一个关联的调用处理程序(InvocationHandler)。对待代理实例进行调用时,将对方法的调用进行编码并指派到它的调用处理器(InvocationHandler)的invoke方法。所以对代理对象实例方法的调用都是通过InvocationHandler中的invoke方法来完成的,而invoke方法会根据传入的代理对象、方法名称以及参数决定调用代理的哪个方法。

3.3.2 newProxyInstance

当我们需要构建一个动态代理对象时,调用了 Proxy 的静态方法:

1
2
3
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

该方法通过代理对象实现的接口,从指定 classloader 中,获得实现这些接口的动态代理类的类对象,其中构造方法存在一个类型为 InvocationHandler 的参数,即直接通过 h 就可以生成动态代理对象。

Proxy 类中使用 ClassLoaderValue 存储已经生成的代理类的 Constructor<?>。同时该 cache 还传入了 proxy 类对象的工厂类,属于懒加载的 LoadingCache:

1
private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

其映射关系就是 classLoader -> { interfaces -> proxyClass , … }

3.3.3 ProxyClassFactory

当 proxyClassCache 中找不到目标 proxy 的类的类对象时,就会调用 实现了 BiFunction<ClassLoader, Class<?>[], Class<?>> 接口的 ProxyClassFactory 的 apply 方法来生成 proxy 类对象(jdk 11 使用的 ProxyBuilder 来构建):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// prefix for all proxy class names
private static final String proxyClassNamePrefix = "$Proxy";

// next number to use for generation of unique proxy class names
private static final AtomicLong nextUniqueNumber = new AtomicLong();

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
/*
* Verify that the class loader resolves the name of this
* interface to the same Class object.
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
/*
* Verify that the Class object actually represents an
* interface.
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*
* Verify that this interface is not a duplicate.
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}
String proxyPkg = null; // package to define proxy class in
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
/*
* Record the package of a non-public proxy interface so that the
* proxy class will be defined in the same package. Verify that
* all non-public proxy interfaces are in the same package.
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
/*
* Generate the specified proxy class.
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}
}

其中注释相当详细,概括其中步骤如下:

  1. 从指定 classloader 中获得传参待实现的接口数组每一个接口的 class
    1. 如果地址不一致,说明要实现的接口的类不在传入的 classLoader 中,抛出异常 IllegalArgumentException( intf + " is not visible from class loader")
    2. 如果非接口类,抛出 IllegalArgumentException( interfaceClass.getName() + " is not an interface")
    3. 如果重复,抛出 IllegalArgumentException("repeated interface: " + interfaceClass.getName())
  2. 检查待实现的接口权限是否 public
    1. 如过不是,检查是多个接口是否同包(如果只有 0-1 个则没问题),否则抛出 IllegalArgumentException( "non-public interfaces from different packages")
    2. 注意接口中只要有一个非 public 的接口, proxy 就会成为 非 public 的类
    3. proxy 类一定是 final 的。
  3. 确定 proxy 的包路径与名称
    1. 包路径默认与第一个接口同包,否则使用 PROXY_PACKAGE_PREFIX = "com.sun.proxy"
    2. 注意 jdk 9 之后还存在 Module 模块,代理类的包名有一定变动
    3. 代理类名称为 "$Proxy" + 【原子long递增】

其中 defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length) 调用 jvm 向 classLoader 载入了 class 对象。看到 0 结尾就猜测到这是一个 native 方法:

1
private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);

3.3.4 generateClassFile() 生成代理类的二进制 class

proxyClassFile 由 ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags) 生成,其中关键方法为 generateClassFile()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
private byte[] generateClassFile() {
/* ============================================================
* Step 1: Assemble ProxyMethod objects for all methods to
* generate proxy dispatching code for.
*/

/*
* Record that proxy methods are needed for the hashCode, equals,
* and toString methods of java.lang.Object. This is done before
* the methods from the proxy interfaces so that the methods from
* java.lang.Object take precedence over duplicate methods in the
* proxy interfaces.
*/
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);

/*
* Now record all of the methods from the proxy interfaces, giving
* earlier interfaces precedence over later ones with duplicate
* methods.
*/
for (Class<?> intf : interfaces) {
for (Method m : intf.getMethods()) {
addProxyMethod(m, intf);
}
}

/*
* For each set of proxy methods with the same signature,
* verify that the methods' return types are compatible.
*/
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}

/* ============================================================
* Step 2: Assemble FieldInfo and MethodInfo structs for all of
* fields and methods in the class we are generating.
*/
try {
methods.add(generateConstructor());

for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {

// add static field for method's Method object
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));

// generate code for proxy method and add it
methods.add(pm.generateMethod());
}
}

methods.add(generateStaticInitializer());

} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}

if (methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
}

/* ============================================================
* Step 3: Write the final class file.
*/

/*
* Make sure that constant pool indexes are reserved for the
* following items before starting to write the final class file.
*/
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for (Class<?> intf: interfaces) {
cp.getClass(dotToSlash(intf.getName()));
}

/*
* Disallow new constant pool additions beyond this point, since
* we are about to write the final constant pool table.
*/
cp.setReadOnly();

ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);

try {
/*
* Write all the items of the "ClassFile" structure.
* See JVMS section 4.1.
*/
// u4 magic;
dout.writeInt(0xCAFEBABE);
// u2 minor_version;
dout.writeShort(CLASSFILE_MINOR_VERSION);
// u2 major_version;
dout.writeShort(CLASSFILE_MAJOR_VERSION);

cp.write(dout); // (write constant pool)

// u2 access_flags;
dout.writeShort(accessFlags);
// u2 this_class;
dout.writeShort(cp.getClass(dotToSlash(className)));
// u2 super_class;
dout.writeShort(cp.getClass(superclassName));

// u2 interfaces_count;
dout.writeShort(interfaces.length);
// u2 interfaces[interfaces_count];
for (Class<?> intf : interfaces) {
dout.writeShort(cp.getClass(
dotToSlash(intf.getName())));
}

// u2 fields_count;
dout.writeShort(fields.size());
// field_info fields[fields_count];
for (FieldInfo f : fields) {
f.write(dout);
}

// u2 methods_count;
dout.writeShort(methods.size());
// method_info methods[methods_count];
for (MethodInfo m : methods) {
m.write(dout);
}

// u2 attributes_count;
dout.writeShort(0); // (no ClassFile attributes for proxy classes)

} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}

return bout.toByteArray();
}

其中注释非常详细,概述如下:

  1. 加入 Object 与所有接口的所有方法到 proxyMethods
    1. addProxyMethod 先加入 Object 的 hashCodeMethod、equalsMethod、toStringMethod 方法,覆盖即多态
    2. 遍历并加入待实现的每一个接口的每一个方法 addProxyMethod(m, intf)
    3. checkReturnTypes 确认签名重复的方法的返回值一致,否则抛出 IllegalArgumentException( “methods with same signature “ + getFriendlyMethodSignature(pm.methodName, pm.parameterTypes) + “ but incompatible return types: “ + newReturnType.getName() + “ and others”);
  2. 组装所有方法到 methods
    1. methods.add(generateConstructor()) 加入构造方法,参数为 InvocationHandler
    2. 加入所有 proxyMethod 的静态私有字段(未初始化)
    3. 加入所有 proxyMethod 的方法(generateMethod() ),其中调用了 InvocationHandler 字段的 invoke 方法(即 toString、hashCode 与 equals 最后调用的 InvocationHandler 的 incoke 来取结果)
    4. methods.add(generateStaticInitializer()) 生成 static 代码块,填充 所有 proxyMethod 的静态私有字段
    5. 检查方法与字段数量,都不能超过 65535(2^16)
  3. 输出 final class 文件到二进制流,按 class 的定义规范进行写入,最终转换成 byte 数组

4. CGLIB 与 ASM

CGLIB(Code Generation Library)它是一个代码生成类库。它可以在运行时候动态是生成某个类的子类。代理模式为要访问的目标对象提供了一种途径,当访问对象时,它引入了一个间接的层。

CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM(Java字节码操控框架),来转换字节码并生成新的类。

CGLIB代理主要通过对字节码的操作,为对象引入间接级别,以控制对象的访问。JDK动态代理虽然简单易用,但是只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了。

需要注意的是,CGLib不能对声明为final的方法进行代理,因为CGLib原理是动态生成被代理类的子类。对于从Object中继承的方法,CGLIB代理也会进行代理,如hashCode()、equals()、toString()等,但是getClass()、wait()等方法不会,因为它是final方法,CGLIB无法代理。

4.1 CGLIB

CGLIB 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。

CGLIB 底层:使用字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

CGLIB缺点:对于final方法,无法进行代理。

Java安全性通过不受信任的代码保护系统资源免受未经授权的访问。 代码可以由签名者标识,代码基url (jar或类文件)可以在本地或从网络下载。 CGLIB生成的类在配置和JVM启动时不存在(在运行时生成),但是所有生成的类都具有与CGLIB本身相同的保护域(签名者和代码库),可以在WS中使用,也可以在带有安全管理器的RMI应用程序中使用。 授予生成类的权限,授予cglib二进制文件的权限。 默认的安全配置在java中。政策文件。这是示例策略文件,它授予cglib和生成代码的所有权限。

1
2
3
4
[java]
grant codeBase "file:${user.dir}/jars/cglib.jar"{
permission java.security.AllPermission;
};

(java 安全策略:/Users/dz0400803/.sdkman/candidates/java/8.0.275.hs-adpt/jre/lib/security/)

详情移步:

如何配置Policy文件进行Java安全策略的设置

4.1.1 Enhancer 与 Callback

Enhancer 类是 CGLib 中的一个字节码增强器,它可以方便的对你想要处理的类进行扩展。

在CGLib回调时可以设置对不同方法执行不同的回调逻辑,或者根本不执行回调。在JDK动态代理中并没有类似的功能,对InvocationHandler接口方法的调用对代理类内的所以方法都有效。

最简单的 Enhancher 可以直接使用静态方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static class Bean {
public String echo(String msg) {
return "echo:" + msg;
}
}

public static void main(String[] args) {
Bean proxy = (Bean) Enhancer.create(Bean.class, new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("enhancer.intercept");
return proxy.invokeSuper(obj, args);
}
});
System.out.println(proxy.echo("hello"));
}

输出:

1
2
enhancer.intercept
echo:hello

等价于:

1
2
3
4
5
6
7
8
9
10
11
Enhancer eh = new Enhancer();
eh.setSuperclass(Bean.class);
eh.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("enhancer.intercept");
return proxy.invokeSuper(obj, args);
}
});
Bean proxy = (Bean) eh.create();
System.out.println(proxy.echo("hello"));

如果想对不同方法使用不同回调,则可以配合 setCallbacks 与 setCallbackFilter 一起使用。其中 setCallbacks 设置了一个回调数组,setCallbackFilter 配置了 method -> 【回调数组下标】 的映射方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class TargetObj {
public String echo0(String msg) {
return "echo0:" + msg;
}
public String echo1(String msg) {
return "echo1:" + msg;
}
public String echo2(String msg) {
return "echo2:" + msg;
}
}

public class MyInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] params, MethodProxy proxy) throws Throwable {
System.out.println("intercept before");
Object result = proxy.invokeSuper(obj, params);
System.out.println("intercept after");
return result;
}
}

public class MyFixedInterceptor implements FixedValue {
@Override
public Object loadObject() throws Exception {
System.out.println("MyFixedInterceptor");
return "fixed";
}
}

public class CglibMain {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TargetObj.class);
// 0:no operator | 1:锁定方法返回值 | 2:方法拦截器
Callback[] cbs = new Callback[] { NoOp.INSTANCE, new MyFixedInterceptor(), new MyInterceptor() };
enhancer.setCallbacks(cbs);
enhancer.setCallbackFilter(m -> {
String name = m.getName();
if (name.matches("echo\\d")) {
return Integer.parseInt(name.substring(4));
}
return 0;
});
TargetObj enhancedObj = (TargetObj) enhancer.create();
System.out.println(enhancedObj.getClass().getName() + "@" + enhancedObj.hashCode());
System.out.println(">>");
System.out.println(enhancedObj.echo0("hello0"));
System.out.println(">>");
System.out.println(enhancedObj.echo1("hello1"));
System.out.println(">>");
System.out.println(enhancedObj.echo2("hello2"));
}
}

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
com.yupaopao.cglib.TargetObj$$EnhancerByCGLIB$$2c89b5d0@1225439493
>>
echo0:hello0
>>
MyFixedInterceptor
fixed
>>
intercept before
intercept after
echo2:hello2
<<<

注意,在上面的示例中,任何方法调用都将被委托,也将调用java.lang.Object中定义的方法。

查看 Callback 接口,可以看到共有以下子接口:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* All callback interfaces used by {@link Enhancer} extend this interface.
* @see MethodInterceptor 方法拦截
* @see NoOp 直接调用 super 方法
* @see LazyLoader 延时加载:加载一次
* @see Dispatcher 延时加载:每次更新
* @see InvocationHandler invoke 拦截
* @see FixedValue 定值,小心出现 ClassCastException
*/
public interface Callback
{
}

4.1.2 懒(延时)加载

LazyLoader 与 Dispatcher 接口继承了 Callback,因此也算是 CGLib 中的一种 Callback 类型。

Dispatcher 和 LazyLoader 的区别在于:LazyLoader 只在第一次访问延迟加载属性时触发代理类回调方法,而 Dispatcher 在每次访问延迟加载属性时都会触发代理类回调方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class LoaderMain {
public static class Prop {
private String val;

@Override
public String toString() {
return val;
}
}

public static class Bean {
private Prop lazyProp;
private Prop dispatcherProp;
private int lCount = 0;
private int dCount = 0;

public Bean() {
LazyLoader lazyLoader = () -> {
Prop p = new Prop();
p.val = "lazy count=" + ++lCount;
return p;
};
Dispatcher dispatcherLoader = () -> {
Prop p = new Prop();
p.val = "dispatcher count=" + ++dCount;
return p;
};
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Prop.class);
lazyProp = (Prop) enhancer.create(Prop.class, lazyLoader);
dispatcherProp = (Prop) enhancer.create(Prop.class, dispatcherLoader);
}

@Override
public String toString() {
return "lazyProp=" + lazyProp + ",dispatcherProp=" + dispatcherProp + ",lCount=" + lCount + ",dCount="
+ dCount;
}
}

public static void main(String[] args) {
Bean b = new Bean();
System.out.println(b);
System.out.println(b);
System.out.println(b);
}
}

输出结果:

1
2
3
lazyProp=lazy count=1,dispatcherProp=dispatcher count=1,lCount=1,dCount=1
lazyProp=lazy count=1,dispatcherProp=dispatcher count=2,lCount=1,dCount=2
lazyProp=lazy count=1,dispatcherProp=dispatcher count=3,lCount=1,dCount=3

4.1.3 InterfaceMaker

InterfaceMaker 会动态生成一个接口,该接口包含指定类定义的所有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class InterfaceMain {
public static interface Inter {
void inter();
}

public static abstract class Abs {
public abstract void abs();

protected abstract void absP();

public void absM() {
}

protected void absPM() {
}

protected final void absFM() {
}
}

public static class MyClass extends Abs implements Inter {
public String say() {
return "s";
}

public void publicM() {
}

public final void publicFM() {
}

private void privateM() {
}

protected void protectedM() {
}

@Override
public void inter() {
}

@Override
public void abs() {
}

@Override
protected void absP() {
}
}

public static void main(String[] args) {
InterfaceMaker interfaceMaker = new InterfaceMaker();
interfaceMaker.add(MyClass.class);
Class<?> targetInterface = interfaceMaker.create();
for (Method method : targetInterface.getMethods()) {
System.out.println(method.getName());
}
System.out.println(targetInterface.getClass().getName());
}
}

输出结果:

1
2
3
4
5
6
7
absM
say
publicM
publicFM
inter
abs
java.lang.Class

可以看到,不论是父类还是接口,只要是 public 的方法都会抽取出来,而 protected/private 的方法不会。另外,其自身的 public final 会被抽出,但父类的 public final 不会。

4.1.4 常见问题

4.1.4.1 StackOverflowError

使用 InvocationHandler 时会对所有调用都将使用相同的 InvocationHandler 进行分派,因此可能导致无休止的循环:

1
2
3
4
5
6
7
8
Bean enhancer = (Bean) Enhancer.create(Bean.class, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("enhancer.invoke");
return method.invoke(proxy, args);
}
});
enhancer.xxx();

为了避免这种情况,我们可以使用另一个回调分派器: MethodInterceptor。但是在使用中,需要注意必须使用 invokesuperer 方法来调用超类方法。如果超方法是抽象的,它将抛出 AbstractMethodError:

1
2
3
4
5
6
7
Object intercept(Object proxy, Method method,
MethodProxy fastMethod, Object args[]) throws Throwable {
//ERROR
System.out.println(proxy.toString());
//ERROR 应该使用 invokesuperer
return fastMethod.invoke(proxy,args);
}

4.1.4.2 OutOfMemoryError:PermSize Space 内存溢出

当增强器创建一个类时,它将为每个拦截器设置一个 private static 字段,其初始值为空。使用 cglib 创建的类定义在创建后不能重用(不能直接 new 它生成的 class ,而是需要借助 Enhancer 的 create() 方法),因为注册回调不会成为生成类初始化阶段的一部分,而是在类已经被JVM初始化之后由 cglib 手工准备。 MethodInterceptor 将会触发额外的类的创建并在增强的类中注册额外的字段:

1
2
3
4
5
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
// setCallBack() 添加的 MethodInterceptor
private MethodInterceptor CGLIB$CALLBACK_0;
//....更多其他字段

因此回调变量存储在增强器的缓存中,因此使用 Enhancer 创建大量实例时,推荐使用同一个 Enhancher 调用 create() 方法,而不是创建大量 Enhancher。另一种方式是维护 Enhancher 中注册的 Callback 单例子。还有一种方式就是重写 hashCode 与 equals 方法来使用缓存。调用 enhancher.setUseCache(true); 可以控制启用与否(默认为 true)。

4.2 ASM

对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。

为了更快地理解ASM的处理流程,需要先了解访问者模式。简单来说,访问者模式主要用于修改或操作一些数据结构比较稳定的数据。

4.2.1 ASM 核心API

ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:

  • ClassReader:用于读取已经编译好的.class文件。
  • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
  • 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。

ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。

4.2.2 ASM AOP DEMO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class Base {
public void process() {
System.out.println("process");
}
}

public class AsmAopMain {
public static class MyClassVisitor extends ClassVisitor implements Opcodes {
public MyClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
// 跳过构造方法 <init> 后,将需要被增强的方法交给内部类MyMethodVisitor来进行处理
if (!name.equals("<init>") && mv != null) {
mv = new MyMethodVisitor(mv);
}
return mv;
}

class MyMethodVisitor extends MethodVisitor implements Opcodes {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}

/**
* 在 ASM 开始访问方法的 Code 区时被调用
*/
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}

/**
* 每当 ASM 访问到无参数指令时,都会调用 MethodVisitor 中的 visitInsn 方法
* 我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中
*/
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
// 方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
}
}

public static void main(String[] args) throws Exception {
// 读取
ClassReader classReader = new ClassReader("com/yupaopao/diyjava/asm/Base");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 处理
ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
// 输出
File f = new File("/Users/dz0400803/Documents/bixin/repos/diy-java/src/main/java/com/yupaopao/diyjava/asm/Base.class");
FileOutputStream fout = new FileOutputStream(f);
fout.write(data);
fout.close();
System.out.println("success");
}
}

生成的 class 文件:

1
2
3
4
5
6
7
public class Base {
public void process() {
System.out.println("start");
System.out.println("process");
System.out.println("end");
}
}

4.3 Spring AOP

Spring AOP也是对目标类增强,生成代理类。但是与AspectJ的最大区别在于—Spring AOP的运行时增强,而AspectJ是编译时增强。

做一个简单的实验我们就可以发现,如果我们使用spring aop来对某一个service进行切面处理,那么调用getClass()方法获得的结果就是:

1
Myservice$$EnhancerBySpringCGLIB$$3afc9148

显然,虽然spring aop采用了aspectj语法来定义切面,但是在实现切面逻辑的时候还是采用CGLIB来进行动态代理的方法。

值得注意的是,动态代理的类不能再通过 new 对象的方式产生 aop 效果,而应该从代理容器中获取对象。

4.3.1 execution 语法

格式:

1
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)  throws-pattern?)
  1. 拦截任意公共方法 execution(public (..))
  2. 拦截以set开头的任意方法 execution( set(..))
  3. 拦截类或者接口中的方法 execution( com.xyz.service.AccountService.(..))
  4. 拦截包或者子包中定义的方法 execution( com.xyz.service...*(..))

4.3.2 spring aop 原理概述

用到的包:aspectjweaver 与 spring-aop

Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。

cglib 获取代理对象的 proxy 工厂在 CglibAopProxy 中,其原理通过 Enhancer 新建的代理对象;JDK 生成代理对象的工厂则是 JdkDynamicAopProxy ,其中 getProxy(classLoader) 获取代理类的方法如下:

1
2
3
4
5
6
7
8
9
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isDebugEnabled()) {
logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

JdkDynamicAopProxy 本身实现了 InvokationHandler 接口,在 Proxy.newProxyInstance(classLoader, proxiedInterfaces, this) 处将自己传为处理器。其中主流程可以简述为:获取可以应用到此方法上的通知链(Interceptor Chain),如果有则应用通知并执行joinpoint;;如果没有则直接反射执行 joinpoint。更细节的实现不再展开。

7 小结

  • AspectJ
    • 三个包:
      • aspectjrt.jar 包主要是提供运行时的一些注解,静态方法等
      • aspectjtools.jar 包主要是 ajc 编译器
      • aspectjweaverjar 包主要是提供了一个 java agent 用于在类加载期间织入切面(Load time weaving,LTW)
    • aspect 关键字与其他语法
    • 三种织入方式
      • 编译时织入:ajc 编译
      • 编译后织入:javac 编译完成后,用ajc再去处理class文件得到新的、织入过切面的class文件
      • 加载时织入(LTW):通过 java agent 机制在内存中操作类文件
  • JDK 动态代理
    • Proxy.newProxyInstance 三个参数:类加载器,接口数组,InvocationHandler
    • 不代理 final 方法,无法代理实现类,只能代理接口
    • jdk 动态代理原理
      • WeakCache(jdk 8,11 为 ClassLoaderValue) 存储已经生成的代理类
      • ProxyClassFactory 生成代理类的类对象(jdk 11 使用的 ProxyBuilder 来构建),构造方法的唯一参数为 InvocationHandler
      • generateClassFile() 生成代理类的二进制 class,先加入 Object 的 hashCodeMethod、equalsMethod、toStringMethod 方法
    • 小心递归调用导致栈溢出
    • 注意重写 hashCode 与 equals 方法
  • CGLIB
    • 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法
    • 六类 Callback
      • MethodInterceptor 方法拦截
      • NoOp 直接调用 super 方法
      • LazyLoader 延时加载:加载一次
      • Dispatcher 延时加载:每次更新
      • InvocationHandler invoke 拦截
      • FixedValue 定值,小心出现 ClassCastException
    • 常见问题
      • StackOverflowError:Callback 里循环调用增强类的方法,触发了 Callback 自身,导致循环调用
      • OutOfMemoryError:Callback 最好使用单例
  • ASM
    • 它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为
    • 使用基于访问者模式
    • Core API:类比解析 XML 文件的 SAX 方式,不需要把这个类的整个结构读取进来
    • Tree API:类比解析 XML 文件的 DOM 方式,缺点是消耗内存多,但是编程比较简单
    • ClassReader / ClassWrite
    • Visitor类:MethodVisitor、FieldVisitor、AnnotationVisitor 等,重点要使用的是 MethodVisitor
  • Spring AOP
    • 包:aspectjweaver 与 spring-aop,注解使用 aspectjweaver,实现使用两种方式来生成代理对象: JDKProxy 和 Cglib
      • cglib 获取代理对象的 proxy 工厂在 CglibAopProxy 中,其原理通过 Enhancer 新建的代理对象
      • JDK 生成代理对象的工厂则是 JdkDynamicAopProxy ,本身实现了 InvokationHandler 接口

8 参考